/**
 * Copyright (C) 2010-2016 eBusiness Information, Excilys Group
 * Copyright (C) 2016-2020 the AndroidAnnotations project
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed To in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package org.ohosannotations.helper;

import com.helger.jcodemodel.AbstractJClass;
import com.helger.jcodemodel.IJExpression;
import com.helger.jcodemodel.JBlock;
import com.helger.jcodemodel.JExpr;
import com.helger.jcodemodel.JFieldRef;
import com.helger.jcodemodel.JInvocation;
import com.helger.jcodemodel.JVar;

import org.ohosannotations.ElementValidation;
import org.ohosannotations.handler.MethodInjectionHandler;
import org.ohosannotations.holder.GeneratedClassHolder;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;

/**
 * 注入助手
 *
 * @param <T> 子类泛型
 * @author dev
 * @since 2021-07-22
 */
public class InjectHelper<T extends GeneratedClassHolder> {
    private final Map<ExecutableElement, List<ParamHelper>> methodParameterMap = new HashMap<>();
    private final Map<ExecutableElement, JBlock> methodBlockMap = new HashMap<>();

    private final ValidatorHelper validatorHelper;
    private final MethodInjectionHandler<T> handler;
    private final APTCodeModelHelper codeModelHelper;

    /**
     * 注入助手
     *
     * @param validatorHelper 验证器辅助
     * @param handler 处理程序
     */
    public InjectHelper(ValidatorHelper validatorHelper, MethodInjectionHandler<T> handler) {
        this.codeModelHelper = new APTCodeModelHelper(validatorHelper.environment());
        this.validatorHelper = validatorHelper;
        this.handler = handler;
    }

    /**
     * 验证
     *
     * @param expectedAnnotation 预期的注释
     * @param element 元素
     * @param valid 有效的
     */
    public void validate(Class<? extends Annotation> expectedAnnotation, Element element, ElementValidation valid) {
        Element enclosingElement = element.getEnclosingElement();
        if (element instanceof VariableElement && enclosingElement instanceof ExecutableElement) {
            validatorHelper.param.annotatedWith(expectedAnnotation)
                .multiple().validate((ExecutableElement) enclosingElement, valid);
            validatorHelper.doesNotHaveAnyOfSupportedAnnotations(enclosingElement, valid);
            handler.validateEnclosingElement(enclosingElement, valid);

        } else if (element instanceof ExecutableElement) {
            handler.validateEnclosingElement(element, valid);
            validatorHelper.param.anyType().validate((ExecutableElement) element, valid);
            List<? extends VariableElement> parameters = ((ExecutableElement) element).getParameters();
            for (VariableElement param : parameters) {
                validatorHelper.doesNotHaveAnyOfSupportedAnnotations(param, valid);
            }

        } else {
            handler.validateEnclosingElement(element, valid);
            validatorHelper.isNotFinal(element, valid);
        }
    }

    /**
     * getParam
     *
     * @param element element
     * @return element
     */
    public Element getParam(Element element) {
        if (element instanceof ExecutableElement) {
            return ((ExecutableElement) element).getParameters().get(0);
        }
        return element;
    }

    /**
     * process
     *
     * @param element element
     * @param holder holder
     */
    public void process(Element element, T holder) {
        if (element instanceof ExecutableElement) {
            processMethod(element, holder);
        } else {
            Element enclosingElement = element.getEnclosingElement();
            if (enclosingElement instanceof ExecutableElement) {
                processParam(element, holder);
            } else {
                processField(element, holder);
            }
        }
    }

    /**
     * processParam
     *
     * @param element element
     * @param holder holder
     */
    private void processParam(Element element, T holder) {
        ExecutableElement method = (ExecutableElement) element.getEnclosingElement();
        List<? extends VariableElement> parameters = method.getParameters();
        List<ParamHelper> parameterList = methodParameterMap.get(method);
        JBlock targetBlock = methodBlockMap.get(method);
        int paramCount = parameters.size();

        if (parameterList == null) {
            parameterList = new ArrayList<>();
            methodParameterMap.put(method, parameterList);
        }
        if (targetBlock == null) {
            targetBlock = createBlock(holder, true);
            methodBlockMap.put(method, targetBlock);
        }

        for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
            VariableElement param = parameters.get(paramIndex);
            if (param.equals(element)) {
                AbstractJClass type = codeModelHelper.typeMirrorToJClass(param.asType());
                JVar fieldRef = targetBlock.decl(type, param.getSimpleName().toString(), getDefault(param.asType()));

                handler.assignValue(targetBlock, fieldRef, holder, param, param);
                parameterList.add(new ParamHelper(fieldRef, paramIndex, param));
            }
        }

        if (parameterList.size() == paramCount) {
            String methodName = method.getSimpleName().toString();

            Collections.sort(parameterList);

            JInvocation invocation = JExpr.invoke(methodName);
            for (ParamHelper parameter : parameterList) {
                invocation.arg(parameter.beanInstance);
            }
            targetBlock.add(invocation);

            if (handler instanceof MethodInjectionHandler.AfterAllParametersInjectedHandler<?>) {
                ((MethodInjectionHandler.AfterAllParametersInjectedHandler<T>) handler)
                    .afterAllParametersInjected(holder, method, parameterList);
            }

            methodParameterMap.remove(method);
        }
    }

    /**
     * processField
     *
     * @param element element
     * @param holder holder
     */
    private void processField(Element element, T holder) {
        String fieldName = element.getSimpleName().toString();
        JFieldRef fieldRef = JExpr._this().ref(fieldName);

        handler.assignValue(createBlock(holder, false),
            fieldRef, holder, element, element);
    }

    /**
     * processMethod
     *
     * @param element element
     * @param holder holder
     */
    private void processMethod(Element element, T holder) {
        ExecutableElement executableElement = (ExecutableElement) element;
        VariableElement param = executableElement.getParameters().get(0);
        String methodName = executableElement.getSimpleName().toString();

        JBlock block = createBlock(holder, true);
        AbstractJClass type = codeModelHelper.typeMirrorToJClass(param.asType());
        JVar fieldRef = block.decl(type, param.getSimpleName()
            .toString(), getDefault(param.asType()));
        handler.assignValue(block, fieldRef, holder, element, param);
        block.add(JExpr.invoke(methodName).arg(fieldRef));
    }

    /**
     * getDefault
     *
     * @param typeMirror typeMirror
     * @return JExpr
     */
    private IJExpression getDefault(TypeMirror typeMirror) {
        switch (typeMirror.toString()) {
            case "int":
                return JExpr.lit(0);
            case "float":
                return JExpr.lit(0f);
            case "double":
                return JExpr.lit(0d);
            case "long":
                return JExpr.lit(0L);
            case "short":
                return JExpr.lit((short) 0);
            case "char":
                return JExpr.lit((char) 0);
            case "byte":
                return JExpr.lit((byte) 0);
            case "boolean":
                return JExpr.lit(false);

            default:
                return JExpr._null();
        }
    }

    /**
     * createBlock
     *
     * @param holder holder
     * @param requiresBracers requiresBracers
     * @return handler
     */
    private JBlock createBlock(T holder, boolean requiresBracers) {
        if (requiresBracers) {
            return handler.getInvocationBlock(holder).block();
        } else {
            return handler.getInvocationBlock(holder);
        }
    }

    /**
     * param助手
     * ParamHelper
     *
     * @author dev
     * @since 2021-07-23
     */
    public static class ParamHelper implements Comparable<ParamHelper> {
        private final int argumentOrder;
        private final Element parameterElement;
        private final IJExpression beanInstance;

        ParamHelper(IJExpression beanInstance, int argumentOrder, Element parameterElement) {
            this.beanInstance = beanInstance;
            this.argumentOrder = argumentOrder;
            this.parameterElement = parameterElement;
        }

        /**
         * getArgumentOrder
         *
         * @return argumentOrder
         */
        public int getArgumentOrder() {
            return argumentOrder;
        }

        /**
         * getParameterElement
         *
         * @return parameterElement
         */
        public Element getParameterElement() {
            return parameterElement;
        }

        /**
         * getBeanInstance
         *
         * @return beanInstance
         */
        public IJExpression getBeanInstance() {
            return beanInstance;
        }

        /**
         * compareTo
         *
         * @param o1 o
         * @return argumentOrder
         */
        @Override
        public int compareTo(ParamHelper o1) {
            return this.argumentOrder - o1.argumentOrder;
        }

        /**
         * equals
         *
         * @param o1 o
         * @return argumentOrder
         */
        @Override
        public boolean equals(Object o1) {
            if (this == o1){
                return true;
            }
            if (o1 == null || getClass() != o1.getClass()){
                return false;
            }
            ParamHelper that = (ParamHelper) o1;
            return argumentOrder == that.argumentOrder &&
                Objects.equals(parameterElement, that.parameterElement) &&
                Objects.equals(beanInstance, that.beanInstance);
        }

        /**
         * hashCode
         *
         * @return Objects
         */
        @Override
        public int hashCode() {
            return Objects.hash(argumentOrder, parameterElement, beanInstance);
        }
    }
}
